#include "Dir_syncer.h"
#include "messages/SyncInitMessage.h"
#include "messages/OfferMessage.h"
#include "messages/SyncControlMessage.h"
#include "../exceptions/not_found_exception.h"
#include "Sync_File.h"
#include "Sync_Regular_File.h"
#include "Sync_Directory.h"
#include "../UI_multiplexer.h"

/* constructor */
Dir_syncer::Dir_syncer(Userdatabase* users, Transport* trans, Filetransfer* filetransfer) :
    users(users),
    trans(trans),
    filetransfer(filetransfer)
{
    trans->register_message_handler(this, TransportMessage::SYNC);
    trans->register_conn_stat_notification(this);    
    this->ui = &UI_multiplexer::instance(); //FIXME always use singleton
}

/* destructor */
Dir_syncer::~Dir_syncer()
{
    // TODO Auto-generated destructor stub
}


// MessageHandlerInterface
void Dir_syncer::handleMessage(TransportMessage *msg, TransportMessage::MESSAGE_TYPE type, uint8_t subtype, UserPtr user)
{
    switch ( subtype )
    {
        // INIT message
        case (INIT):
        {
            got_init(msg, user);
            break;
        }

        // OFFER message
        case (OFFER):
        {
            got_offer(msg, user);
            break;
        }
        
        // CANCEL message
        case (CANCEL):
        {
            got_cancel(msg, user);
            break;
        }


        // nothing.. delete ourselves.
        default:
        {
            delete msg;
            break;
        }
    }

}


/** PRIVATE **/
/** handler for incoming sync-init packets **/
void Dir_syncer::got_init(TransportMessage* msg, UserPtr user)
{
    SyncInitMessage* m = msg->decapsulate<SyncInitMessage>();

    // get sync object from init message
    SyncPtr sync = m->get_sync();
    sync->set_partner(user);

    // XXX debug output (ggf echten user output draus machen)
    ostringstream out;
    out << "GOT SYNC INIT: »" << sync->get_composed_tag() << "«, " << sync->get_their_path() << ", " << sync->get_mine_path() << ", " << sync->get_partner()->get_name();


    try
    {
        // BRANCH: INITIATOR: SYNC INIT ACK  --->  begin outgoing transfers
        if ( m->is_ack() )
        {
            out << " [ACK]";
            UI.write_info(out.str());

            try
            {
                SyncPtr xsync = get_sync_from_db(sync);

                UI.write_info(":-)");
                xsync->set_receiver_accepted(true);

                // register this sync as transfer-bundle
                filetransfer->register_bundle(xsync);

                // looking for files we have to send (and register to stay informed)
                xsync->register_update_listener(this);
                sync_updated_callback(xsync);  // TODO zwischenfunktion aufrufen (o.ä.) ?
            }
            catch (not_found_exception& e)
            {
                ostringstream out;
                out << "ERROR! Got invalid SYNC-ACK: " << e.what();
                UI.write_warn(out.str());
            }
        }


        // BRANCH: got NEW SYNC from another user
        else
        {
            UI.write_info(out.str());

            // sync types --> handle different types different
            switch ( sync->get_sync_type() )
            {
                case Sync::PUSH_SYNC:
                {
                    got_push_sync_init(sync);

                    break;
                }

                case Sync::PULL_SYNC:
                {
                    got_pull_sync_init(sync);

                    break;
                }
                default:
                {
                    UI.write_warn("ERROR: Got invalid sync init.");

                    break;
                }
            }

            // TODO PUSH SYNC sollte auf «allow push» hören (ggf. User-interaktion) !!
            // dann kann man das folgende auch gleich so umbauen, dass man "minePath" asynchron auf existenz checken kann..

            // XXX debug output
            ostringstream out2;
            out2 << "===> TAG: »" << sync->get_composed_tag() << "«, THEIR PATH: " << sync->get_their_path() << ", MINE PATH: " << sync->get_mine_path() << ", PARTNER: " << sync->get_partner()->get_name();
            UI.write_info(out2.str());


            // insert sync into database
            bool ok = add_sync(sync);

            // ERR: tag collision!
            // (since the partner obviously has no collision, abort old sync)
            if ( ! ok )
            {
                ;  // TODO abort old sync

        //        add_sync(sync);
                UI.write_warn("ERROR! Handling of Tag-Collision not yet implemented..");
            }

            // register this sync as transfer-bundle
            filetransfer->register_bundle(sync);

            // * ack sync init message *
            SyncInitMessage ans(sync, true);
            ok = trans->send_to(ans, TransportMessage::SYNC, INIT, user);


            // ERR:
            if ( ! ok )
            {
                UI.write_warn("ERROR! Can't send ACK.");
            }


            // begin directory scanning
            sync->register_update_listener(this);
            sync->scan();
        }
    }
    catch (runtime_error& e)
    {
        ostringstream out;
        out << "ERROR! " << e.what();
        UI.write_warn(out.str());

        // TODO gegenseite bescheid sagen, oder eben gerade nicht?
    }

    delete m;
}


void Dir_syncer::got_push_sync_init(SyncPtr sync)
{
    // TODO hier hätte ich gern wirklich eine Klasse "security_exception"

    // assert: RECEIVER
    if ( sync->get_role() != Sync::RECEIVER )
        throw runtime_error("SECURITY_EXCEPTION: push sync expects to be a receiver");

    // assert: no destination path set
    if ( sync->get_mine_path() != "" )
        throw runtime_error("SECURITY_EXCEPTION: push sync expects the local path to be unset");



    // a little bit of security.. --> check sync-name
    path name = sync->get_name().filename();
    if ( name.empty() || name.string()[0] == '.' )
    {
        throw runtime_error("SECURITY_EXCEPTION: invalid name");
    }

    // create an empty local path
    // TODO was wenn es Ordner schon gibt?  --> asynchron checken.. :-(
    // TODO "incoming/" nicht hardcodieren. Außerdem andere Orte denkbar..
    path mpath = path("incoming") / path(sync->get_partner()->get_masked_name()) / name;


    // --> set path & name
    sync->set_mine_path(mpath);
    sync->set_name(name);
}

void Dir_syncer::got_pull_sync_init(SyncPtr sync)
{
    // TODO hier hätte ich gern wirklich eine Klasse "security_exception"

    // assert: SENDER
    if ( sync->get_role() != Sync::SENDER )
        throw runtime_error("SECURITY_EXCEPTION: push sync expects to be a receiver");


    // check privilege
    if ( ! sync->get_partner()->file_has_privilege(sync->get_mine_path(), Access_attributes::PULL) )
    {
        throw runtime_error("SECURITY_EXCEPTION: no right to PULL '" + sync->get_mine_path().string() + "'");
    }
}



void Dir_syncer::got_offer(TransportMessage* msg, UserPtr user)
{
    SyncUpdateMessage* m = msg->decapsulate<SyncUpdateMessage>();

    // iterate over offered files  // XXX debug output..
    SyncFileList file_list = m->get_filelist();
    UI.write_info("FILES UPDATED FROM »" + user->get_name() + "« for '" + m->get_tag() + "':");
    for ( list<SyncFilePtr>::iterator it = file_list->begin(); it != file_list->end(); it++ )
    {
        SyncRegularFilePtr file = boost::static_pointer_cast<Sync_Regular_File> (*it);
        string status = file->get_status_as_string();
//        if ( file->get_status() == Sync_File::UNDEFINED )
//            status = "UNDEFINED";
//        else if ( file->get_status() == Sync_File::READY_TO_RECEIVE )
//            status = "READY_TO_RECEIVE";
//        else if ( file->get_status() == Sync_File::READY_TO_SEND )
//            status = "READY_TO_SEND";

        UI.write_info(file->get_base_path().string() + " --> " + file->get_common_path().string() + " " + status);
    }

    // let the sync-object handle these files
    try
    {
        SyncPtr sync = get_sync_from_db(m->get_tag(), user);
        UI.write_info("Handle Updates. (SYNC: " + sync->get_mine_path().string() + ")");
        sync->handle_update_from_partner(m->get_filelist());
    }
    catch (not_found_exception& e)
    {
        UI.write_warn("Got update for unknown sync. (USER: " + user->get_name() + ", TAG: " + m->get_tag() + ")");
        UI.write_warn(e.what());

        // send cancel, so the other side will forget about this sync, too.
        SyncControlMessage msg(m->get_tag());
        trans->send_to(msg, TransportMessage::SYNC, CANCEL, user);
    }

    delete m;
}


void Dir_syncer::got_cancel(TransportMessage* msg, UserPtr user)
{
    SyncControlMessage* m = msg->decapsulate<SyncControlMessage>();

    // find sync object and cancel
    try
    {
        SyncPtr sync = drop_sync_from_db(m->get_tag(), user);
        UI.write_info("Sync canceled. (SYNC: " + sync->get_mine_path().string() + ")");
        
        // TODO aktuell CANCEL!
        
    }
    catch (not_found_exception& e)
    {
        UI.write_warn("Got cancel for unknown sync. (USER: " + user->get_name() + ", TAG: " + m->get_tag() + ")");
    }
    
    // TODO cancel ack?
    
    delete m;
}
/** [handler for incoming packets] **/



void Dir_syncer::initiate_push_sync(UserPtr partner, string minePath)
{
    // assert: partner is_connected
    if ( ! partner->is_connected() )
    {
        UI.write_warn("please connect first!");
        return;
    }

    // OK: directory exists
    boost::filesystem::path xpath(minePath);
    if ( boost::filesystem::is_directory(xpath) )  // TODO, soll auch für Dateien gehen..
    {
        // new sync object
        SyncPtr sync(new Sync());
        sync->set_sync_type_and_role(Sync::PUSH_SYNC, Sync::SENDER);
        sync->set_partner(partner);
        sync->set_mine_path(xpath);
        sync->set_sane_name_from_path(xpath);
        sync->set_their_path("");

        // creates tag and insert sync into database
        string tag = create_unique_tag(sync);
        bool ok = add_sync(sync);

        // user output
        ostringstream out;
        out << "[SYNC] initiating push sync: '" << minePath << "' " << "(" << sync->get_tag() << ") with " + partner->get_name() << " [waiting for acceptance]";
        UI.write_to_user(out.str());

        // * send sync init message *
        SyncInitMessage msg(sync, false);
        ok &= trans->send_to(msg, TransportMessage::SYNC, INIT, partner);


        // Error handling
        if ( ! ok )
        {
            // this should never happen!!
            UI.write_warn("FATAL Error! Can't send SYNC_INIT message. Please send in a bug report. :-/");
        }


        // begin directory scanning
        sync->scan();
    }

    // ERR: directory does not exist
    else
    {
      UI.write_warn("Directory: '" + minePath + "' does not exist!");
    }

}


void Dir_syncer::initiate_pull_sync(UserPtr partner, string theirPath)
{
    // assert: partner is_connected
    if ( ! partner->is_connected() )
    {
        UI.write_warn("please connect first!");
        return;
    }

    // new sync object, create mine_path
    SyncPtr sync(new Sync());
    sync->set_sync_type_and_role(Sync::PULL_SYNC, Sync::RECEIVER);
    sync->set_partner(partner);
    sync->set_their_path(theirPath);
    sync->set_sane_name_from_path(theirPath);
    sync->set_mine_path(path("incoming") / path(partner->get_masked_name()) / sync->get_name());

    // creates tag and insert sync into database
    string tag = create_unique_tag(sync);
    bool ok = add_sync(sync);

    // user output
    ostringstream out;
    out << "[SYNC] initiating pull sync: '" << theirPath << "' " << "(" << sync->get_tag() << ") with " + partner->get_name() << " [waiting for acceptance]";
    UI.write_to_user(out.str());

    // * send sync init message *
    SyncInitMessage msg(sync, false);
    ok &= trans->send_to(msg, TransportMessage::SYNC, INIT, partner);


    // Error handling
    if ( ! ok )
    {
        // this should never happen!!
        UI.write_warn("FATAL Error! Can't send SYNC_INIT message. Please send in a bug report. :-/");
    }


    // begin directory scanning (not much to do, since it's empty)
    sync->scan();
}




void Dir_syncer::sync_updated_callback(SyncPtr sync)
{
    // TODO check if the sync has been canceled!
    
    
    // if partner is connected, send him the new information
    if ( sync->get_partner()->is_connected() )
    {
        // get unlimited number of new files
        SyncFileList new_list = sync->get_updated_files(-1);  // TODO limit!! for scalability

        // XXX debug, aber vielleicht weiter runter und tatsächlich den status setzen..
        for ( list<SyncFilePtr>::iterator it = new_list->begin(); it != new_list->end(); it++ )
        {
            SyncRegularFilePtr file = boost::static_pointer_cast<Sync_Regular_File> (*it);
            string status;
            if ( file->get_status() == Sync_File::UNDEFINED )
                status = "UNDEFINED";
            else if ( file->get_status() == Sync_File::READY_TO_RECEIVE )
                status = "READY_TO_RECEIVE";
            else if ( file->get_status() == Sync_File::READY_TO_SEND )
                status = "READY_TO_SEND";


            UI.write_info("Send Offer-Message: " + (*it)->get_base_path().string() + " --> " + (*it)->get_common_path().string() + " " + status);  // XXX
            //file->set_status ....  //TODO
        }

        // * send offer message *
        SyncUpdateMessage msg(sync->get_tag(), new_list);
        bool ok = trans->send_to(msg, TransportMessage::SYNC, OFFER, sync->get_partner());


    }
}


// TODO uU name ändern... (auch im Interface)
void Dir_syncer::activate_transfers_callback(boost::shared_ptr<Sync> sync)
{
    UI.write_info("Beginning Transfers for: " + sync->get_composed_tag());

    filetransfer->bundle_announce_files_waiting(sync);

//    // XXX nur Testcode, das darf hier nicht bleiben
//    FilePtr file;
//    do
//    {
//        file = sync->get_next_file();
//
//        if ( file )
//            UI.write_to_user("FILE: " + file->get_filename());
//    } while ( file );
//
//    sync->transfers_ended_notification();
}


void Dir_syncer::got_connection_update(UserPtr user, bool connected)
{
    // TODO extra index?
    // TODO irgendwie in funktionen aufteilen?

    // find syncs with this user
    for ( SyncMap::iterator it=sync_db.begin(); it != sync_db.end(); it++ )
    {
        SyncPtr sync = it->second;

        // TODO aktuell: sync->sleep() und sync->scan() oder so..
        // und für connection updates anmelden..
        if ( sync->get_partner()->get_name() == user->get_name() )  // TODO operator== für user..?
        {
            // BRANCH: CONNECTED
            if (connected)
            {
                // FIXME aktuell: Gegenseite fragen ob sie noch interessiert ist..?
                sync->scan(); // TODO scan aufrufen und überlegen, was schief gehen kann (Karin)
            }

            // BRANCH: DISCONNECTED
            else
            {
                sync->sleep();
            }
        }
    }

}


// XXX doch nicht?
void Dir_syncer::user_connected(UserPtr user)
{

}

void Dir_syncer::user_disconnected(UserPtr user)
{

}


string Dir_syncer::create_unique_tag(SyncPtr sync)
{
    boost::filesystem::path xpath(sync->get_mine_path());
    string filename = xpath.filename().string();
    string tag = filename;
    UserPtr user = sync->get_partner();

    int i = 0;
    bool collision;
    do
    {
        collision = false;

        // check candidate
        SyncMap::iterator it;
        it = sync_db.find(Sync::COMPOSE_TAG(tag, user));

        // collision: tag already in db
        if ( it != sync_db.end() )
        {
            collision = true;

            // change candidate -> append number and count it up
            ostringstream cat;
            cat << filename << i++;
            tag = cat.str();
        }

    } while ( collision );


    // * set tag *
    sync->set_tag(tag);

    return tag;
}


/** Sync DB **/ 
// add
bool Dir_syncer::add_sync(SyncPtr sync)
{
    // insert
    pair<SyncMap::iterator, bool> ret = sync_db.insert( std::pair<string, SyncPtr>(sync->get_composed_tag(), sync) );

    return ret.second;
}

// get
SyncPtr Dir_syncer::_get_sync_from_db(string unique_tag)
{
    SyncMap::iterator it;
    it = sync_db.find(unique_tag);

    // Err: not found
    if ( it == sync_db.end() )
        throw not_found_exception("Tag '" + unique_tag + "' not in DB");

    return it->second;
}

SyncPtr Dir_syncer::get_sync_from_db(SyncPtr sync)
{
    return _get_sync_from_db(sync->get_composed_tag());
}

SyncPtr Dir_syncer::get_sync_from_db(string base_tag, UserPtr user)
{
    return _get_sync_from_db(Sync::COMPOSE_TAG(base_tag, user));
}

// drop
SyncPtr Dir_syncer::_drop_sync_from_db(string unique_tag)
{
    SyncMap::iterator it;
    it = sync_db.find(unique_tag);

    // Err: not found
    if ( it == sync_db.end() )
        throw not_found_exception("Tag '" + unique_tag + "' not in DB");

    SyncPtr s = it->second;
    sync_db.erase(it);
    
    return s;
}

SyncPtr Dir_syncer::drop_sync_from_db(string base_tag, UserPtr user)
{
    return _drop_sync_from_db(Sync::COMPOSE_TAG(base_tag, user));
}
/** [Sync DB] **/

void Dir_syncer::send_next_file(SyncPtr sync)
{

}



